<?php
namespace Libsignal\kdf;

use Exception;

abstract class HKDF{

    const HASH_OUTPUT_SIZE = 32;

    /**
     * @param $version
     * @return HKDFv2|HKDFv3
     * @throws Exception
     */
    public static function createFor($version)
    {
        if ($version == 2) {
            return new HKDFv2();
        }
        if ($version == 3) {
            return new HKDFv3();
        }
        throw new Exception("Unknown version $version");
    }

    public function deriveSecrets($inputKey, $info, $outputLength, $salt = null)
    {
        $salt = ($salt != null ? $salt : str_repeat(chr(0), self::HASH_OUTPUT_SIZE));
        $prk = $this->extract($salt, $inputKey);

        return $this->expand($prk, $info, $outputLength);
    }

    public function extract($salt, $inputKey)
    {
        $mac = hash_init('sha256', HASH_HMAC, $salt);
        hash_update($mac, $inputKey);

        return hash_final($mac, true);
    }

    public function expand($prk, $info, $outputSize)
    {
        $iterations = (int) ceil(floatval($outputSize) / floatval(self::HASH_OUTPUT_SIZE));
        $remainingBytes = $outputSize;
        $mixin = '';
        $result = '';
        for ($i = $this->getIterationStartOffset(); $i < $iterations + $this->getIterationStartOffset(); $i++) {
            $mac = hash_init('sha256', HASH_HMAC, $prk);
            hash_update($mac, $mixin);
            if ($info != null) {
                hash_update($mac, $info);
            }
            $updateChr = chr($i % 256);
            hash_update($mac, $updateChr);
            $stepResult = hash_final($mac, true);
            $stepSize = min($remainingBytes, strlen($stepResult));
            $result .= substr($stepResult, 0, $stepSize);
            $mixin = $stepResult;
            $remainingBytes -= $stepSize;
        }

        return $result;
    }

    public abstract function getIterationStartOffset();

}
